<?php

namespace app\lib\installer;

use core\lib\tool\File;
use core\lib\upload\UploadFile as LibUploadFile;
use Exception;
use PhpZip\ZipFile;

/**
 * 通用安装器
 * 
 * 手动安装包升级包格式 命名规则 组件名称-版本号.zip
 * cms-1.0.0.zip
 * 
 * 升级包格式 多版本 换行    升级版本号|升级地址
 * 1.0.0-1.0.1|http://frameupgrade.rovecoder.com/plugin/upgrade/cms/cms-1.0.0~1.0.1.zip
 * 1.0.1-1.0.2|http://frameupgrade.rovecoder.com/plugin/upgrade/cms/cms-1.0.1~1.0.2.zip
 */
class General
{

    /**
     * 获取升级列表
     *
     * @return array
     */
    public function getUpgradeList(string $upgradeurl): array
    {
        $upgradeContent = file_get_contents($upgradeurl);

        // 处理不同的换行符 兼容问题
        return  explode("\n", str_replace(["\r\n", "\r"], ["\n", "\n"], $upgradeContent));
    }

    /**
     * 获取软件的名称 不含版本号
     *
     * @param string $url 升级或安装URL地址 获取压缩包名称 然后解析出组件名称
     * @return string
     */
    public function getNameNoVersion(string $url): string
    {
        $pathinfo   = pathinfo($url);
        $spacenames = explode('-', $pathinfo['filename']);
        unset($spacenames[count($spacenames) - 1]);
        return  implode('-', $spacenames);
    }

    public function getNameAndVersion(string $url): array
    {
        $pathinfo      = pathinfo($url);

        $spacenames    = explode('-', $pathinfo['filename']);
        $versionAndExt = end($spacenames); //版本（包含后缀）
        unset($spacenames[count($spacenames) - 1]);
        $name = implode('-', $spacenames); //软件名

        $versionarr = explode('~', $versionAndExt);
        return [
            'name'        => $name,
            'fromversion' => $versionarr[0],
            'toversion'   => isset($versionarr[1]) ? $versionarr[1] : ''
        ];
    }

    /**
     * 获取下一个版本
     *
     * @param string $upgradeurl 升级文件地址
     * @param string $nowVersion 当前版本
     * @return array 下一个版本号,升级地址 ,组件插件名称
     */
    public function getNextUpgradeVersion(string $upgradeurl, string $nowVersion): array
    {
        $upgradeList = $this->getUpgradeList($upgradeurl);
        foreach ($upgradeList as $upgradeItem) {
            if (trim($upgradeItem) == '') {
                continue;
            }
            list($versions, $url) = explode('|', $upgradeItem);
            list($fromVersion, $toVersion) = explode('~', $versions);
            if ($fromVersion == $nowVersion && $toVersion != '') {
                $name = $this->getNameNoVersion($url);
                return [
                    'nextVersion' => $toVersion,
                    'upgradeUrl'  => $url,
                    'name'        => $name
                ];
            }
        }
        return [];
    }




    /**
     * 检查是否安装
     *
     * @param string $installpath 安装路径
     * @param string $name 组件名称
     * @param bool $fullname 是否完整名称
     * @param bool $fromuploadgetname 是否上传文件中获取文件名
     * @return bool
     */
    public function isInstalled(
        string $installpath,
        string $name = '',
        bool $fullname = true,
        bool $fromuploadgetname = true,
    ): bool {

        //得到软件最终的名称 不含版本号和后缀
        if ($name == '' && $fromuploadgetname) {
            $spl_file = array_values(request()->file());
            $name = $this->getNameNoVersion($spl_file[0]->getUploadName());
        } else {
            if ($fullname) {
                $name = $this->getNameNoVersion($name);
            }
        }
        echo rtrim($installpath, '/') . '/' . $name;
        return file_exists(rtrim($installpath, '/') . '/' . $name);
    }


    /**
     * 上传安装/升级压缩包
     *
     * @param string $path 上传路径 在 runtime/storage 目录下 只需要写入插件名即可
     * @return array
     */
    public function uploadZip(string $path): array
    {

        //压缩包上传
        $libUploadFile = new LibUploadFile(
            ispublic: false,
            path: $path
        );

        $fileinfo = $libUploadFile->upload();
        return $fileinfo;
    }

    /**
     * 下载升级压缩包
     * @param string $url 升级地址
     * @param string $savepath 保存路径
     * @return string
     */
    public function downloadUpgradeZip(string $url, string $savepath):string
    {
        return File::downloadRemoteFile($url, $savepath);
    }


    /**
     * 释放压缩包 并检查是否有必要文件
     * 释放到指定目录下 并返回目录路径
     *
     * @param string $zippath 压缩包路径
     * @param string $extractpath 释放路径
     * @param array $requirefiles 压缩包内必备文件
     * @return string
     */
    public function extractZip(
        string $zippath,
        string $extractpath,
        array $requirefiles
    ): string {

        $zipFile = new ZipFile();
        $zipFile->openFile($zippath);
        $filelists = $zipFile->getListFiles();


        //检查压缩包内是否有必备文件
        foreach ($requirefiles as $requirefile) {
            if (!in_array($requirefile, $filelists)) {
                throw new Exception('压缩包错误，缺少必要文件');
            }
        }

        if (!file_exists($extractpath)) {
            mkdir($extractpath, 0777, true);
        }

        $zipFile->extractTo($extractpath);
        $zipFile->close();
        return $extractpath;
    }




    /**
     * 安装
     *
     * @param string $installpath 安装路径
     * @param array $requirefiles 压缩包内必备文件 
     * @return array 解压路径,组件名称
     * @throws Exception
     */
    public function install(
        string $installpath,
        array $requirefiles = []
    ) {
        // 检查是否安装
        if ($this->isInstalled($installpath)) {
            throw new Exception('软件已安装');
        }

        // 上传安装包
        $uploadfileinfo = $this->uploadZip('install');

        // 解压安装包
        $foldername = $this->getNameNoVersion($uploadfileinfo['orgname']);
        $installpath = $this->extractZip(
            base_path() . $uploadfileinfo['path'],
            rtrim($installpath, '/') . '/' . $foldername,
            $requirefiles
        );

        // 清理压缩包
        $this->clearInstallFile(base_path() . $uploadfileinfo['path']);


        return [
            'installpath' => $installpath,
            'foldername'  => $foldername
        ];
    }

    /**
     * 在线升级
     *
     * @param string $upgradeurl 升级远程地址
     * @param string $nowversion 当前版本
     * @param string $upgradepath 升级解压路径 
     * @param array $requirefiles 压缩包内必备文件
     * @return array 解压路径,下一个版本号
     * @throws Exception
     */
    public function onlineUpgrade(
        string $upgradeurl,
        string $nowversion,
        string $upgradepath,
        array $requirefiles = []
    ) {
        $nextVersion = $this->getNextUpgradeVersion($upgradeurl, $nowversion);
        if (!$nextVersion) {
            throw new Exception('没有找到升级包');
        }

        //下载升级包
        $downpath = File::downloadRemoteFile($nextVersion['upgradeUrl'], 'upgrade');

        //解压升级包
        $extractpath = $this->extractZip(
            $downpath,
            rtrim($upgradepath, '/') . '/' . $nextVersion['name'],
            $requirefiles
        );

        //清理升级包
        $this->clearInstallFile($downpath);
        return [
            'extractpath' => $extractpath,
            'nextVersion' => $nextVersion['nextVersion']
        ];
    }

    /**
     * 离线升级
     *
     * @param string $name 要升级的软件名称
     * @param string $nowversion 当前版本
     * @param string $upgradepath 升级解压路径
     * @param array $requirefiles 压缩包内必备文件
     * @return array 解压路径,下一个版本号
     * @throws Exception
     */
    public function offlineUpgrade(
        string $name,
        string $nowversion,
        $upgradepath,
        $requirefiles = []
    ) {

        $uploadfileinfo = $this->uploadZip('upgrade');

        //解压升级包
        $nameAndVersion = $this->getNameAndVersion($uploadfileinfo['orgname']);

        if ($nameAndVersion['name'] != $name) {
            throw new Exception('软件名错误');
        }

        if ($nameAndVersion['fromversion'] != $nowversion) {
            throw new Exception('版本号错误');
        }

        $extractpath = $this->extractZip(
            base_path() . $uploadfileinfo['path'],
            rtrim($upgradepath, '/') . '/' . $nameAndVersion['name'],
            $requirefiles
        );

        //清理升级包
        $this->clearInstallFile(base_path() . $uploadfileinfo['path']);
        return [
            'extractpath' => $extractpath,
            'nextVersion' => $nameAndVersion['toversion']
        ];
    }



    /**
     * 清理安装文件
     *
     * @param string $zippath
     * @return void
     */
    public function clearInstallFile(string $path)
    {
        try {
            if (file_exists($path)) {
                unlink($path);
            }
        } catch (Exception $ex) {
        }
    }
}
